import math
import logging

from collections import deque
from dataclasses import dataclass
from coin.base.datetime_util import to_datetime
from coin.base.timestamp import get_timestamp

@dataclass
class SimpleBookEntry:
    timestamp: float
    exchange: str
    symbol: str
    ask0: float
    bid0: float

# for range_multiplier x, the estimated window will at most has 1/(1+x^2) records out of the window.
class WindowEstimator(object):
  def __init__(self, max_record_cnt = 3600, range_multiplier = 3.0):
    self._kmax_record_cnt = max_record_cnt
    self._krange_multiplier = range_multiplier
    self._cur_record_cnt = 0
    self._cur_mean = 0.0
    self._cur_sqr_mean = 0.0
    self._half_win_size = 0.0
    self._record_queue = deque()
  
  def insert_record(self, price):
    price = float(price)
    if self._cur_record_cnt < self._kmax_record_cnt:
      self._record_queue.append(price)
      self._cur_mean = self._cur_mean * ((float)(self._cur_record_cnt) / (float)(self._cur_record_cnt + 1)) + price / (float)(self._cur_record_cnt + 1)
      self._cur_sqr_mean = self._cur_sqr_mean * ((float)(self._cur_record_cnt) / (float)(self._cur_record_cnt + 1)) + price ** 2 / (float)(self._cur_record_cnt + 1)
      self._cur_record_cnt += 1
    else:
      self._record_queue.append(price)
      poped_price = self._record_queue.popleft()
      self._cur_mean += (price - poped_price) / (float)(self._cur_record_cnt)
      self._cur_sqr_mean += (price**2 - poped_price**2) / (float)(self._cur_record_cnt)
    self._half_win_size = math.sqrt(max(0.0, self._cur_sqr_mean - self._cur_mean ** 2)) * self._krange_multiplier

  def is_empty(self):
    return self._cur_record_cnt <= 0
  
  def has_enough_record(self):
    return self._cur_record_cnt >= self._kmax_record_cnt
  
  def get_avg_price(self):
    return self._cur_mean
  
  def get_half_window_size(self):
    return self._half_win_size

  def get_price_window(self):
    return self._cur_mean - self._half_win_size, self._cur_mean + self._half_win_size
  
  def print_info(self, logger):
    logger.info('record_cnt:%d, cur_mean:%f, cur_sqr_mean:%f, price_low:%f, price_upper:%f' % (
      self._cur_record_cnt, self._cur_mean, self._cur_sqr_mean, self._cur_mean - self._half_win_size, self._cur_mean + self._half_win_size
    ))

class BiasCalculator(object):
  def __init__(self, max_record_cnt = 3600, withdraw_signal_bar = 3.0, provide_signal_bar = 1.0):
    self._kmax_record_cnt = max_record_cnt
    self._kwithdraw_signal_bar = withdraw_signal_bar
    self._kprovide_signal_bar = provide_signal_bar
    self._cur_record_cnt = 0
    self._cur_bias_val = 0.0
    self._record_queue = deque()
  
  def calculate_bias(self, price_diff):
    if price_diff > 0.0:
      return price_diff ** 2
    else:
      return -(price_diff ** 2)

  def insert_record(self, binance_price, uniswap_price):
    price_diff = binance_price - uniswap_price
    if self._cur_record_cnt < self._kmax_record_cnt:
      self._record_queue.append(price_diff)
      self._cur_bias_val = self._cur_bias_val * ((float)(self._cur_record_cnt) / (float)(self._cur_record_cnt + 1)) + self.calculate_bias(price_diff) / (float)(self._cur_record_cnt + 1)
      self._cur_record_cnt += 1
    else:
      self._record_queue.append(price_diff)
      poped_price_diff = self._record_queue.popleft()
      self._cur_bias_val += (self.calculate_bias(price_diff) - self.calculate_bias(poped_price_diff)) / (float)(self._cur_record_cnt)

  def is_empty(self):
    return self._cur_record_cnt <= 0
  
  def has_enough_record(self):
    return self._cur_record_cnt >= self._kmax_record_cnt
  
  def get_bias_val(self):
    return self._cur_bias_val
  
  def is_plus_bias_withdraw(self):
    return self._cur_bias_val > self._kwithdraw_signal_bar
  
  def is_minus_bias_withdraw(self):
    return self._cur_bias_val < -self._kwithdraw_signal_bar

  def is_plus_bias_provide(self):
    return self._cur_bias_val > self._kprovide_signal_bar
  
  def is_minus_bias_provide(self):
    return self._cur_bias_val < -self._kprovide_signal_bar
  
  def should_withdraw(self):
    return self.is_plus_bias_withdraw() or self.is_minus_bias_withdraw()
  
  def should_provide(self):
    return not self.is_plus_bias_provide() and not self.is_minus_bias_provide()

  def print_info(self, logger):
    bias_level = math.sqrt(abs(self._cur_bias_val))
    logger.info('record_cnt:%d, cur_bias_val:%f, bias_level:%f' % (
      self._cur_record_cnt, self._cur_bias_val, bias_level))

@dataclass
class BiasEntry:
    bias: float
    start_price: float
    start_ts: int
    end_ts: int

# group closing bias, and validate if the price 1hr later can be correctly indicated by bias
class BiasValidator(object):
  def __init__(self, bias_group_ts_diff = 600 * 10 ** 9, bias_validate_ts_diff = 3600 * 10 ** 9, bias_bar = 9.0, price_diff_bar = 5.0):
    self._kbias_group_ts_diff = bias_group_ts_diff
    self._kbias_validate_ts_diff = bias_validate_ts_diff
    self._kbias_bar = bias_bar
    self._kprice_diff_bar = price_diff_bar
    self._bias_queue = deque()
    self._current_bias_entry = None
    self._first_start_ts = 0
    self._valid_cnt = 0
    self._invalid_cnt = 0

  def process_bias_entry(self, timestamp, bias, price):
    if abs(bias) < self._kbias_bar and not self._current_bias_entry is None and timestamp - self._current_bias_entry.end_ts > self._kbias_group_ts_diff:
      if self._first_start_ts != 0:
        self._bias_queue.append(self._current_bias_entry)
      self._current_bias_entry = None
      return
    if abs(bias) >= self._kbias_bar:
      if self._current_bias_entry is None:
        self._current_bias_entry = BiasEntry(bias, price, timestamp, timestamp)
        if self._first_start_ts == 0:
          self._first_start_ts = timestamp
      else:
        self._current_bias_entry.end_ts = timestamp

  def is_valid_bias(self, bias, start_price, end_price):
    if bias > 0.0:
      return end_price - start_price >= self._kprice_diff_bar
    else:
      return start_price - end_price >= self._kprice_diff_bar

  def validate_bias_and_update(self, bias, start_price, end_price):
    if self.is_valid_bias(bias, start_price, end_price):
      logging.info("valid bias!!")
      self._valid_cnt += 1
    else:
      logging.info("invalid bias!!")
      self._invalid_cnt += 1

  def validate_bias_entry(self, timestamp, price):
    if self._first_start_ts == 0:
      return
    if timestamp - self._first_start_ts > self._kbias_validate_ts_diff:
      if len(self._bias_queue) > 0:
        current_bias_entry = self._bias_queue.popleft()      
        assert(current_bias_entry.start_ts == self._first_start_ts)
        logging.info("validate bias: bias:%f, start_price:%f, end_price:%f, start_time:%s, end_time:%s" % (
          current_bias_entry.bias, current_bias_entry.start_price, price, to_datetime(current_bias_entry.start_ts), to_datetime(current_bias_entry.end_ts)
        ))
        self.validate_bias_and_update(current_bias_entry.bias, current_bias_entry.start_price, price)
        if len(self._bias_queue) > 0:
          self._first_start_ts = self._bias_queue[0].start_ts
        elif not self._current_bias_entry is None:
          self._first_start_ts = self._current_bias_entry.start_ts
        else:
          self._first_start_ts = 0
      elif not self._current_bias_entry is None:
        assert(self._current_bias_entry.start_ts == self._first_start_ts)
        logging.info("validate bias: bias:%f, start_price:%f, end_price:%f, start_time:%s, end_time:%s" % (
          self._current_bias_entry.bias, self._current_bias_entry.start_price, price, to_datetime(self._current_bias_entry.start_ts), to_datetime(self._current_bias_entry.end_ts)
        ))
        self.validate_bias_and_update(self._current_bias_entry.bias, self._current_bias_entry.start_price, price)
        self._first_start_ts = 0
      else:
        self._first_start_ts = 0
        assert(False)


  def insert_bias(self, timestamp, bias, price):
    self.process_bias_entry(timestamp, bias, price)
    self.validate_bias_entry(timestamp, price)

  def get_working_cnt(self):
    return len(self._bias_queue) + (0 if self._current_bias_entry is None else 1)
  
  def get_valid_cnt(self):
    return self._valid_cnt
  
  def get_all_cnt(self):
    return self._valid_cnt + self._invalid_cnt
  
  def get_valid_ratio(self):
    if self._valid_cnt + self._invalid_cnt <= 0:
      return 0.0
    else:
      return float(self._valid_cnt) / float(self._valid_cnt + self._invalid_cnt)

@dataclass
class BiasEntry2:
    bias: float
    start_price: float
    start_ts: int
    end_ts: int
    valid: bool

# similar to BiasValidator, but if any price in 0hr - 1hr meets the requirement, then the bias is valid.
class BiasValidator2(object):
  def __init__(self, bias_group_ts_diff = 600 * 10 ** 9, bias_validate_ts_diff = 3600 * 10 ** 9, bias_bar = 9.0, price_diff_bar = 5.0):
    self._kbias_group_ts_diff = bias_group_ts_diff
    self._kbias_validate_ts_diff = bias_validate_ts_diff
    self._kbias_bar = bias_bar
    self._kprice_diff_bar = price_diff_bar
    self._bias_queue = deque()
    self._in_processing_bias = False
    self._valid_cnt = 0
    self._invalid_cnt = 0

  def process_bias_entry(self, timestamp, bias, price):
    if len(self._bias_queue) <= 0 and self._in_processing_bias:
      self._in_processing_bias = False
    if abs(bias) < self._kbias_bar and self._in_processing_bias and timestamp - self._bias_queue[-1].end_ts > self._kbias_group_ts_diff:
      self._in_processing_bias = False
      return
    if abs(bias) >= self._kbias_bar:
      if not self._in_processing_bias:
        curent_bias = BiasEntry2(bias, price, timestamp, timestamp, False)
        self._bias_queue.append(curent_bias)
        self._in_processing_bias = True
      else:
        self._bias_queue[-1].end_ts = timestamp

  def is_valid_bias(self, bias, start_price, end_price):
    if bias > 0.0:
      return end_price - start_price >= self._kprice_diff_bar
    else:
      return start_price - end_price >= self._kprice_diff_bar

  def validate_bias_entry(self, timestamp, price):
    for i in range(0, len(self._bias_queue)):
      if self._bias_queue[i].valid:
        continue
      if self.is_valid_bias(self._bias_queue[i].bias, self._bias_queue[i].start_price, price):
        logging.info("bias becomes valid: bias:%f, start_price:%f, end_price:%f, start_time:%s, end_time:%s" % (
          self._bias_queue[i].bias, self._bias_queue[i].start_price, price, to_datetime(self._bias_queue[i].start_ts), to_datetime(self._bias_queue[i].end_ts)
        ))
        self._bias_queue[i].valid = True

    if len(self._bias_queue) > 0 and timestamp - self._bias_queue[0].start_ts > self._kbias_validate_ts_diff:
      current_bias_entry = self._bias_queue.popleft()
      logging.info("bias validation: bias:%f, start_price:%f, start_time:%s, end_time:%s" % (
        current_bias_entry.bias, current_bias_entry.start_price, to_datetime(current_bias_entry.start_ts), to_datetime(current_bias_entry.end_ts)
      ))
      if current_bias_entry.valid:
        logging.info("valid bias!!")
        self._valid_cnt += 1
      else:
        logging.info("invalid bias!!")
        self._invalid_cnt += 1

  def insert_bias(self, timestamp, bias, price):
    self.process_bias_entry(timestamp, bias, price)
    self.validate_bias_entry(timestamp, price)

  def get_working_cnt(self):
    return len(self._bias_queue)
  
  def get_valid_cnt(self):
    return self._valid_cnt
  
  def get_all_cnt(self):
    return self._valid_cnt + self._invalid_cnt
  
  def get_valid_ratio(self):
    if self._valid_cnt + self._invalid_cnt <= 0:
      return 0.0
    else:
      return float(self._valid_cnt) / float(self._valid_cnt + self._invalid_cnt)

class BiasValidator3(object):
  def __init__(self, is_minus_bias = True, bias_group_ts_diff = 600 * 10 ** 9, bias_validate_ts_diff = 3600 * 10 ** 9, bias_bar = 9.0, leave_bias_bar = 2.25, price_diff_bar = 5.0):
    self._kis_minus_bias = is_minus_bias
    self._kbias_group_ts_diff = bias_group_ts_diff
    self._kbias_validate_ts_diff = bias_validate_ts_diff
    self._kbias_bar = bias_bar
    self._kleave_bias_bar = leave_bias_bar
    self._kprice_diff_bar = price_diff_bar
    self._bias_queue = deque()
    self._current_bias_entry = None
    self._in_processing_bias = False
    self._valid_cnt = 0
    self._invalid_cnt = 0
    self._total_profit = 0.0

  def process_bias_entry(self, timestamp, bias, price):
    if self._current_bias_entry is None and ((self._kis_minus_bias and bias <= -self._kbias_bar) or (not self._kis_minus_bias and bias >= self._kbias_bar)):
    #if self._current_bias_entry is None and abs(bias) >= self._kbias_bar:
      self._current_bias_entry = BiasEntry(bias, price, timestamp, timestamp)
      return
    
    if not self._current_bias_entry is None and ((self._kis_minus_bias and bias > -self._kleave_bias_bar) or (not self._kis_minus_bias and bias < self._kleave_bias_bar)):
      current_profit = (price - self._current_bias_entry.start_price) if self._current_bias_entry.bias > 0.0 else (self._current_bias_entry.start_price - price)
      current_profit -= 3.0
      self._total_profit += current_profit
      logging.info("validate bias: bias:%f, start_price:%f, end_price:%f, profit:%f, total_profit:%f, start_time:%s, end_time:%s" % (
        self._current_bias_entry.bias, self._current_bias_entry.start_price, price, current_profit, self._total_profit, to_datetime(self._current_bias_entry.start_ts), to_datetime(timestamp)
      ))
      self.validate_bias_and_update(self._current_bias_entry.bias, self._current_bias_entry.start_price, price)
      self._current_bias_entry = None
      return

  def is_valid_bias(self, bias, start_price, end_price):
    if bias > 0.0:
      return end_price - start_price >= self._kprice_diff_bar
    else:
      return start_price - end_price >= self._kprice_diff_bar

  def validate_bias_and_update(self, bias, start_price, end_price):
    if self.is_valid_bias(bias, start_price, end_price):
      logging.info("valid bias!!")
      self._valid_cnt += 1
    else:
      logging.info("invalid bias!!")
      self._invalid_cnt += 1

  def insert_bias(self, timestamp, bias, price):
    self.process_bias_entry(timestamp, bias, price)

  def get_working_cnt(self):
    return 0 if self._current_bias_entry is None else 1
  
  def get_valid_cnt(self):
    return self._valid_cnt
  
  def get_all_cnt(self):
    return self._valid_cnt + self._invalid_cnt
  
  def get_valid_ratio(self):
    if self._valid_cnt + self._invalid_cnt <= 0:
      return 0.0
    else:
      return float(self._valid_cnt) / float(self._valid_cnt + self._invalid_cnt)